<!--
MIT License

Copyright (c) 2025 久止白蓝

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="SPDX-License-Identifier" content="MIT">
    <title>自行车动力计算器</title>
    <!-- MathJax configuration to process dollar signs for inline math -->
    <script>
        // MathJax configuration to process dollar signs for inline math
        MathJax = {
            tex: {
                inlineMath: [['$', '$'], ['\\(', '\\)']]
            },
            svg: {
                fontCache: 'global'
            }
        };
    </script>
    <!-- MathJax for LaTeX rendering - defer loading to avoid blocking rendering -->
    <script type="text/javascript" id="MathJax-script" async
      src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
    </script>
    <style>
        /* 基础样式 */
        body {
            font-family: 'Inter', sans-serif; /* 使用Inter字体 */
            line-height: 1.6;
            padding: 20px;
            margin: 0;
            transition: background-color 0.3s, color 0.3s;
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh; /* 确保页面至少占满视口高度 */
            box-sizing: border-box;
        }

        /* 根据系统主题调整背景和字体颜色 */
        @media (prefers-color-scheme: light) {
            body {
                background-color: #f4f4f4;
                color: #333;
            }
            .input-group input[type="number"] {
                background-color: #fff;
                color: #333;
                border: 1px solid #ddd;
            }
            h1, h2, h3 {
                color: #28a745; /* 浅色模式下的标题颜色 */
            }
            .result-area {
                background-color: rgba(40, 167, 69, 0.1); /* 浅色模式下的结果区域背景 */
            }
            .result-area strong {
                color: #28a745; /* 浅色模式下的结果强调色 */
            }
            .chart-item {
                background-color: #fff; /* 浅色模式下的图表背景 */
            }
            .reset-button {
                background-color: #28a745;
                color: white;
                border: 1px solid #28a745;
            }
            /* Slider track color for light mode */
            .input-group input[type="range"]::-webkit-slider-runnable-track {
                background: #d3d3d3;
            }
            .input-group input[type="range"]::-moz-range-track {
                background: #d3d3d3;
            }
            .input-group input[type="range"]::-ms-track {
                background: #d3d3d3;
            }
            /* Slider thumb color for light mode */
            .input-group input[type="range"]::-webkit-slider-thumb {
                background: #28a745;
            }
            .input-group input[type="range"]::-moz-range-thumb {
                background: #28a745;
            }
            .input-group input[type="range"]::-ms-thumb {
                background: #28a745;
            }
        }

        @media (prefers-color-scheme: dark) {
            body {
                background-color: #222;
                color: #eee;
            }
            .input-group input[type="number"] {
                background-color: #333;
                color: #eee;
                border: 1px solid #555;
            }
            h1, h2, h3 {
                color: #4CAF50; /* 深色模式下的标题颜色 */
            }
            .result-area {
                background-color: rgba(76, 175, 80, 0.1); /* 深色模式下的结果区域背景 */
            }
            .result-area strong {
                color: #4CAF50; /* 深色模式下的结果强调色 */
            }
            .chart-item {
                background-color: #333; /* 深色模式下的图表背景 */
            }
            .reset-button {
                background-color: #4CAF50;
                color: white;
                border: 1px solid #4CAF50;
            }
            /* Slider track color for dark mode */
            .input-group input[type="range"]::-webkit-slider-runnable-track {
                background: #555;
            }
            .input-group input[type="range"]::-moz-range-track {
                background: #555;
            }
            .input-group input[type="range"]::-ms-track {
                background: #555;
            }
            /* Slider thumb color for dark mode */
            .input-group input[type="range"]::-webkit-slider-thumb {
                background: #4CAF50;
            }
            .input-group input[type="range"]::-moz-range-thumb {
                background: #4CAF50;
            }
            .input-group input[type="range"]::-ms-thumb {
                background: #4CAF50;
            }
        }

        /* 默认深色背景 (如果系统没有明确设置主题) */
        body:not([data-theme="light"]):not([data-theme="dark"]) {
            background-color: #222;
            color: #eee;
        }
        body:not([data-theme="light"]):not([data-theme="dark"]) .input-group input[type="number"] {
            background-color: #333;
            color: #eee;
            border: 1px solid #555;
        }
        body:not([data-theme="light"]):not([data-theme="dark"]) h1,
        body:not([data-theme="light"]):not([data-theme="dark"]) h2,
        body:not([data-theme="light"]):not([data-theme="dark"]) h3 {
            color: #4CAF50;
        }
        body:not([data-theme="light"]):not([data-theme="dark"]) .result-area {
            background-color: rgba(76, 175, 80, 0.1);
        }
        body:not([data-theme="light"]):not([data-theme="dark"]) .result-area strong {
            color: #4CAF50;
        }
        body:not([data-theme="light"]):not([data-theme="dark"]) .chart-item {
            background-color: #333;
        }
        body:not([data-theme="light"]):not([data-theme="dark"]) .reset-button {
            background-color: #4CAF50;
            color: white;
            border: 1px solid #4CAF50;
        }
        /* Slider track color for default dark mode */
        body:not([data-theme="light"]):not([data-theme="dark"]) .input-group input[type="range"]::-webkit-slider-runnable-track {
            background: #555;
        }
        body:not([data-theme="light"]):not([data-theme="dark"]) .input-group input[type="range"]::-moz-range-track {
            background: #555;
        }
        body:not([data-theme="light"]):not([data-theme="dark"]) .input-group input[type="range"]::-ms-track {
            background: #555;
        }
        /* Slider thumb color for default dark mode */
        body:not([data-theme="light"]):not([data-theme="dark"]) .input-group input[type="range"]::-webkit-slider-thumb {
            background: #4CAF50;
        }
        body:not([data-theme="light"]):not([data-theme="dark"]) .input-group input[type="range"]::-moz-range-thumb {
            background: #4CAF50;
        }
        body:not([data-theme="light"]):not([data-theme="dark"]) .input-group input[type="range"]::-ms-thumb {
            background: #4CAF50;
        }


        .container {
            max-width: 900px;
            width: 100%;
            padding: 20px;
            box-sizing: border-box;
        }

        /* Main title at the very top */
        .main-title {
            text-align: center;
            margin-top: 0;
            margin-bottom: 20px;
            width: 100%; /* Ensure it spans full width */
            padding-bottom: 10px; /* Add some padding below */
            border-bottom: 2px solid #4CAF50; /* A line below the title */
        }

        h2, h3 {
            text-align: center;
            margin-bottom: 20px;
        }

        .section-title {
            text-align: left;
            margin-top: 30px;
            margin-bottom: 15px;
            border-bottom: 2px solid #4CAF50;
            padding-bottom: 5px;
        }

        .input-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); /* Two columns on wider screens */
            gap: 15px;
            margin-bottom: 20px;
        }

        .input-group {
            display: flex;
            flex-direction: column;
            position: relative; /* For slider positioning */
        }

        .input-group label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
            white-space: nowrap; /* Prevent label wrapping */
            cursor: pointer; /* Make labels clickable */
        }

        /* Slider styling */
        .input-group .slider-container {
            display: flex;
            align-items: center;
            gap: 10px; /* Space between number input and slider */
            margin-top: 5px; /* Space below the label */
        }

        .input-group input[type="number"] {
            flex: 3; /* Occupy 3 parts of the space */
            padding: 10px;
            border-radius: 8px;
            font-size: 16px;
            box-sizing: border-box; /* Include padding and border in width */
            transition: border-color 0.3s, box-shadow 0.3s;
        }

        .input-group input[type="number"]:focus {
            border-color: #4CAF50;
            box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
            outline: none;
        }

        .input-group input[type="range"] {
            flex: 7; /* Occupy 7 parts of the space */
            -webkit-appearance: none; /* Override default look */
            appearance: none;
            height: 8px;
            border-radius: 5px;
            outline: none;
            opacity: 0.7;
            transition: opacity .2s;
            cursor: pointer;
        }

        .input-group input[type="range"]:hover {
            opacity: 1;
        }

        .input-group input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            cursor: grab;
            box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
            transition: background-color 0.3s, box-shadow 0.3s;
        }

        .input-group input[type="range"]::-moz-range-thumb {
            width: 20px;
            height: 20px;
            border-radius: 50%;
            cursor: grab;
            box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
            transition: background-color 0.3s, box-shadow 0.3s;
        }

        .input-group input[type="range"]::-ms-thumb {
            width: 20px;
            height: 20px;
            border-radius: 50%;
            cursor: grab;
            box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
            transition: background-color 0.3s, box-shadow 0.3s;
        }

        .input-group input[type="range"]:active::-webkit-slider-thumb,
        .input-group input[type="range"]:active::-moz-range-thumb,
        .input-group input[type="range"]:active::-ms-thumb {
            cursor: grabbing;
        }

        hr {
            border: none;
            border-top: 1px solid #555;
            margin: 30px 0;
        }

        .result-area {
            margin-top: 20px;
            padding: 15px;
            border-radius: 8px;
            text-align: center;
        }

        .result-area strong {
            font-size: 1.5em;
        }

        .result-area .note {
            font-size: 0.9em;
            color: #888;
            margin-top: 10px;
        }

        .chart-grid {
            display: flex;
            flex-wrap: wrap; /* Allow wrapping on smaller screens */
            gap: 20px;
            justify-content: center;
            margin-top: 30px;
            width: 100%;
        }

        .chart-item {
            flex: 1; /* Allows items to grow and shrink */
            min-width: 300px; /* Minimum width before wrapping */
            max-width: 48%; /* Max width for two columns */
            height: 400px; /* Fixed height for charts */
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 10px;
        }

        .chart-item h3 {
            margin-top: 5px;
            margin-bottom: 15px;
        }

        .chart-item canvas {
            width: 100% !important;
            height: calc(100% - 40px) !important; /* Adjust height for title */
        }

        /* New header for parameter controls */
        .params-controls-header {
            display: flex;
            flex-direction: column; /* Default to column for mobile first */
            align-items: center;
            width: 100%;
            margin-bottom: 20px; /* Space below this section */
        }

        .params-controls-header .input-description {
            margin: 0; /* Remove default paragraph margin */
            text-align: center;
            flex-grow: 1; /* Allow text to take available space */
            padding-right: 15px; /* Space between text and button */
        }

        .reset-button {
            padding: 8px 15px; /* Smaller padding for a smaller button */
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px; /* Smaller font size */
            transition: background-color 0.3s, border-color 0.3s, box-shadow 0.3s;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            flex-shrink: 0; /* Prevent button from shrinking */
        }

        .reset-button:hover {
            opacity: 0.9;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
        }

        /* Style for formula containers to allow horizontal scrolling */
        .formula-container {
            overflow-x: auto; /* Allow horizontal scrolling for overflowing content */
            padding-bottom: 5px; /* Add some padding for the scrollbar */
            margin-bottom: 10px; /* Space between formulas */
            text-align: center; /* Center the formulas if they fit */
        }


        /* 移动端优化 */
        @media (min-width: 769px) { /* For larger screens, arrange header elements horizontally */
            .params-controls-header {
                flex-direction: row; /* Arrange horizontally */
                justify-content: space-between; /* Push text to left, button to right */
                align-items: center; /* Vertically align items in the middle */
            }
            .params-controls-header .input-description {
                text-align: left; /* Align description to the left */
                white-space: nowrap; /* Prevent wrapping on desktop */
            }
            .chart-item {
                max-width: 48%; /* Max width for two columns */
            }
            .input-grid {
                grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); /* Two columns on wider screens */
            }
            .input-group .slider-container {
                flex-direction: row; /* Arrange horizontally on wider screens */
            }
        }
        @media (max-width: 768px) {
            .chart-item {
                max-width: 100%; /* Stack charts vertically on small screens */
                height: 300px; /* Adjust height for mobile */
            }
            .input-grid {
                grid-template-columns: 1fr; /* Single column on small screens */
            }
            .params-controls-header .input-description {
                padding-right: 0; /* Remove padding on mobile */
            }
            .input-group .slider-container {
                flex-direction: column; /* Stack vertically on small screens */
                align-items: flex-start; /* Align items to the start */
            }
            .input-group input[type="number"] {
                width: 100%; /* Full width for number input on small screens */
                flex: none; /* Remove flex on small screens to let width take precedence */
            }
            .input-group input[type="range"] {
                width: 100%; /* Full width for slider on small screens */
                flex: none; /* Remove flex on small screens to let width take precedence */
            }
        }
        @media (max-width: 600px) {
            body {
                padding: 10px;
            }
            .container {
                padding: 10px;
            }
        }
    </style>
</head>
<body>
    <h1 class="main-title">自行车动力计算器</h1>

    <div class="container">
        <div class="params-controls-header">
            <p class="input-description">请在下方输入框中调整参数：</p>
            <button id="reset-params-btn" class="reset-button">重置参数</button>
        </div>

        <div id="calculator-app">
            <div class="input-grid">
                <div class="input-group">
                    <label for="tire-size">轮胎尺寸 (英寸):</label>
                    <div class="slider-container">
                        <input type="number" id="tire-size" value="27.5" step="0.1" min="10" max="30">
                        <input type="range" id="tire-size-slider" value="27.5" step="0.1" min="10" max="30">
                    </div>
                </div>
                <div class="input-group">
                    <label for="gear-ratio">牙盘传动比 (前/后):</label>
                    <div class="slider-container">
                        <input type="number" id="gear-ratio" value="1" step="0.1" min="0.1" max="5">
                        <input type="range" id="gear-ratio-slider" value="1" step="0.1" min="0.1" max="5">
                    </div>
                </div>
                <div class="input-group">
                    <label for="slope">坡度 (度):</label>
                    <div class="slider-container">
                        <input type="number" id="slope" value="17.5" step="0.1" min="0" max="90">
                        <input type="range" id="slope-slider" value="17.5" step="0.1" min="0" max="90">
                    </div>
                </div>
                <div class="input-group">
                    <label for="weight">人车总重量 (kg):</label>
                    <div class="slider-container">
                        <input type="number" id="weight" value="100" step="1" min="10" max="500">
                        <input type="range" id="weight-slider" value="100" step="1" min="10" max="500">
                    </div>
                </div>
                <div class="input-group">
                    <label for="speed">行进速度 (km/h):</label>
                    <div class="slider-container">
                        <input type="number" id="speed" value="10" step="0.1" min="0.1" max="60">
                        <input type="range" id="speed-slider" value="10" step="0.1" min="0.1" max="60">
                    </div>
                </div>
                <div class="input-group">
                    <label for="rolling-resistance">滚动阻力 (%):</label>
                    <div class="slider-container">
                        <input type="number" id="rolling-resistance" value="5" step="0.1" min="0" max="20">
                        <input type="range" id="rolling-resistance-slider" value="5" step="0.1" min="0" max="20">
                    </div>
                </div>
                <div class="input-group">
                    <label for="air-density">空气密度 (kg/m³):</label>
                    <div class="slider-container">
                        <input type="number" id="air-density" value="1.225" step="0.001" min="0.5" max="2.0">
                        <input type="range" id="air-density-slider" value="1.225" step="0.001" min="0.5" max="2.0">
                    </div>
                </div>
                <div class="input-group">
                    <label for="frontal-area">迎风面积 (m²):</label>
                    <div class="slider-container">
                        <input type="number" id="frontal-area" value="0.5" step="0.01" min="0.1" max="2.0">
                        <input type="range" id="frontal-area-slider" value="0.5" step="0.01" min="0.1" max="2.0">
                    </div>
                </div>
                <div class="input-group">
                    <label for="wind-speed">风速 (km/h, 顺风为负):</label>
                    <div class="slider-container">
                        <input type="number" id="wind-speed" value="0" step="0.1" min="-30" max="30">
                        <input type="range" id="wind-speed-slider" value="0" step="0.1" min="-30" max="30">
                    </div>
                </div>
                <div class="input-group">
                    <label for="drivetrain-efficiency">传动效率 (%):</label>
                    <div class="slider-container">
                        <input type="number" id="drivetrain-efficiency" value="95" step="0.1" min="50" max="100">
                        <input type="range" id="drivetrain-efficiency-slider" value="95" step="0.1" min="50" max="100">
                    </div>
                </div>
            </div>

            <hr>

            <div class="result-area">
                <h3>计算结果：</h3>
                <p>中轴所需瞬时动力: <strong id="power-output">0.00</strong> 瓦特 (W)</p>
                <p>克服重力所需动力: <strong id="gravity-power-output">0.00</strong> 瓦特 (W)</p>
                <p>克服滚动阻力所需动力: <strong id="rolling-power-output">0.00</strong> 瓦特 (W)</p>
                <p>克服空气阻力所需动力: <strong id="air-power-output">0.00</strong> 瓦特 (W)</p>
                <p>踏频: <strong id="cadence-output">0.00</strong> RPM</p>
                <p class="note">注：瓦时 (Wh) 是能量单位，计算需要乘以骑行时间。此处显示为瞬时功率。</p>
            </div>

            <div class="chart-grid">
                <div class="chart-item">
                    <h3>所需动力随坡度变化</h3>
                    <canvas id="slopePowerChart"></canvas>
                </div>
                <div class="chart-item">
                    <h3>所需动力随速度变化</h3>
                    <canvas id="speedPowerChart"></canvas>
                </div>
            </div>
        </div>

        <hr>

        <h2 class="section-title">功能与公式</h2>
        <p>本网页提供一个交互式工具，用于计算自行车在爬坡时中轴所需的动力。用户可以自定义输入各项参数，并实时查看计算结果。</p>

        <h3>主要功能：</h3>
        <ul>
            <li><strong>自定义输入：</strong> 用户可以调整轮胎尺寸、牙盘传动比、坡度、人车总重量、行进速度、滚动阻力百分比、空气密度、迎风面积、风速和传动效率。</li>
            <li><strong>动态调整：</strong> 在输入框上使用鼠标滚轮可以动态调整数值。</li>
            <li><strong>实时计算：</strong> 所有参数修改后，计算结果会即时更新。</li>
            <li><strong>可视化图表：</strong> 同时显示所需动力随坡度变化和随速度变化的图表，鼠标悬停时显示具体数值。</li>
            <li><strong>重置参数：</strong> 点击“重置参数”按钮可将所有输入值恢复为默认设置。</li>
            <li><strong>响应式设计：</strong> 针对移动设备进行了优化。</li>
            <li><strong>主题跟随系统：</strong> 页面背景颜色根据用户系统主题自动调整，默认深色模式。</li>
            <li><strong>图表缩放与平移：</strong> 鼠标在图表区域滚动时，可以对坐标系进行放大和缩小；拖动图表可进行平移。</li>
            <li><strong>参数浮窗图表：</strong> 点击参数标题（如坡度、人车总重量等）时，弹出该参数与所需动力或踏频关系的图表。</li>
            <li><strong>滑块调整：</strong> 每个参数输入框旁边都增加了滑块，方便更直观地调整参数。</li>
        </ul>

        <h3>使用公式：</h3>
        <p>自行车爬坡所需的总动力 ($P_{total}$) 主要由克服空气阻力 ($P_{air}$)、克服滚动阻力 ($P_{roll}$) 和克服重力 ($P_{gravity}$) 三部分组成。</p>
        <p>1.  <strong>坡度转弧度：</strong></p>
        <p class="formula-container">$\alpha_{rad} = \text{坡度} \times \frac{\pi}{180}$</p>
        <p>2.  <strong>克服重力所需动力 ($P_{gravity}$):</strong></p>
        <p class="formula-container">$P_{gravity} = \text{总重量} \times g \times \text{速度} \times \sin(\alpha_{rad})$</p>
        <p>其中：</p>
        <ul>
            <li>总重量：人车总重量 (kg)</li>
            <li>$g$：重力加速度，取 9.8 $m/s^2$</li>
            <li>速度：自行车行进速度 (m/s)</li>
            <li>$\alpha_{rad}$：坡度对应的弧度</li>
        </ul>
        <p>3.  <strong>克服滚动阻力所需动力 ($P_{roll}$):</strong></p>
        <p class="formula-container">$P_{roll} = \text{总重量} \times g \times \text{速度} \times \text{滚动阻力百分比}$</p>
        <p>其中：</p>
        <ul>
            <li>总重量：人车总重量 (kg)</li>
            <li>$g$：重力加速度，取 9.8 $m/s^2$</li>
            <li>速度：自行车行进速度 (m/s)</li>
            <li>滚动阻力百分比：一个无量纲的百分比值 (例如 5% = 0.05)</li>
        </ul>
        <p>4.  <strong>克服空气阻力所需动力 ($P_{air}$):</strong></p>
        <p class="formula-container">$P_{air} = 0.5 \times \text{空气密度} \times \text{迎风面积} \times C_d \times (\text{速度}_{自行车} + \text{风速}_{相对})^3$</p>
        <p>其中：</p>
        <ul>
            <li>空气密度：空气的密度 (kg/m³)</li>
            <li>迎风面积：自行车和骑行者的迎风面积 (m²)</li>
            <li>$C_d$：空气阻力系数（或称风阻系数），无量纲，通常取 0.6</li>
            <li>速度$_{自行车}$：自行车行进速度 (m/s)</li>
            <li>风速$_{相对}$：与自行车方向相反的风速 (m/s)。顺风时为负值，逆风时为正值。</li>
        </ul>
        <p>5.  <strong>总动力（车轮处） ($P_{wheel}$):</strong></p>
        <p class="formula-container">$P_{wheel} = P_{gravity} + P_{roll} + P_{air}$</p>
        <p>6.  <strong>中轴所需动力 ($P_{crank}$):</strong></p>
	<p>中轴所需动力是车轮处总动力除以传动效率，当($P_{wheel}$)为负数时采用乘法。</p>
        <p class="formula-container">$P_{crank} = \frac{P_{wheel}}{\text{传动效率百分比}}$</p>
        <p class="formula-container">$P_{crank} = P_{wheel} \times \text{传动效率百分比}$</p>
        <p>其中：</p>
        <ul>
            <li>传动效率百分比：一个无量纲的百分比值 (例如 95% = 0.95)</li>
        </ul>
        <p>7.  <strong>能量单位转换：</strong></p>
        <p>计算结果通常以瓦特 (W) 为单位，瓦时 (Wh) 是能量单位，表示功率乘以时间。由于问题要求计算“中轴产生的动力是多少瓦时”，这暗示需要一个时间单位。在物理学中，功率是做功的速率，单位是瓦特 (W)，即焦耳/秒 (J/s)。如果需要瓦时 (Wh)，则需要乘以小时数。如果问题原意是要求瞬时功率，那么结果就是瓦特 (W)。考虑到自行车运动是持续的，我们提供瞬时功率 (W) 的结果，因为瓦时 (Wh) 还需要一个骑行时长才能计算。</p>
        <p>8.  <strong>踏频 (Cadence) 计算：</strong></p>
        <p class="formula-container">踏频 ($RPM$) = $\frac{\text{行进速度 (m/s)}}{\text{车轮周长 (m)}} \times \frac{1}{\text{牙盘传动比}} \times 60$</p>
        <p>其中：</p>
        <ul>
            <li>行进速度 (m/s)：自行车行进速度 (m/s)</li>
            <li>车轮周长 (m)：根据轮胎尺寸计算得出，即 $\pi \times \text{轮胎直径 (m)}$</li>
            <li>牙盘传动比：定义为前盘齿数与后盘齿数之比（$\text{前盘齿数} / \text{后盘齿数}$）。例如，如果前盘有 44 齿，后盘有 22 齿，则传动比为 2。</li>
        </ul>
        <p><strong>注：</strong> 牙盘传动比和轮胎尺寸主要影响在给定速度下，踏频和扭矩的关系，不直接影响克服外力所需的总功率。</p>
    </div>

    <!-- Defer loading of Chart.js and its plugin to improve initial page load performance -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js" defer></script>
    <!-- Chart.js Zoom Plugin for better zoom-pan functionality - defer loading -->
    <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@1.2.1/dist/chartjs-plugin-zoom.min.js" defer></script>

    <script defer>
        document.addEventListener('DOMContentLoaded', () => {
            // Get all input elements and result display elements
            const tireSizeInput = document.getElementById('tire-size');
            const gearRatioInput = document.getElementById('gear-ratio');
            const slopeInput = document.getElementById('slope');
            const weightInput = document.getElementById('weight');
            const speedInput = document.getElementById('speed');
            const rollingResistanceInput = document.getElementById('rolling-resistance');
            const airDensityInput = document.getElementById('air-density');
            const frontalAreaInput = document.getElementById('frontal-area');
            const windSpeedInput = document.getElementById('wind-speed');
            const drivetrainEfficiencyInput = document.getElementById('drivetrain-efficiency');

            // Get slider elements
            const tireSizeSlider = document.getElementById('tire-size-slider');
            const gearRatioSlider = document.getElementById('gear-ratio-slider');
            const slopeSlider = document.getElementById('slope-slider');
            const weightSlider = document.getElementById('weight-slider');
            const speedSlider = document.getElementById('speed-slider');
            const rollingResistanceSlider = document.getElementById('rolling-resistance-slider');
            const airDensitySlider = document.getElementById('air-density-slider');
            const frontalAreaSlider = document.getElementById('frontal-area-slider');
            const windSpeedSlider = document.getElementById('wind-speed-slider');
            const drivetrainEfficiencySlider = document.getElementById('drivetrain-efficiency-slider');


            const powerOutput = document.getElementById('power-output');
            const gravityPowerOutput = document.getElementById('gravity-power-output');
            const rollingPowerOutput = document.getElementById('rolling-power-output');
            const airPowerOutput = document.getElementById('air-power-output');
            const cadenceOutput = document.getElementById('cadence-output');

            const resetParamsBtn = document.getElementById('reset-params-btn'); // Get reset button

            // Get canvas contexts for both charts
            const slopeChartCtx = document.getElementById('slopePowerChart').getContext('2d');
            const speedChartCtx = document.getElementById('speedPowerChart').getContext('2d');

            // Store default parameter values
            const defaultParams = {
                tireSize: 27.5,
                gearRatio: 1,
                slope: 17.5,
                weight: 100,
                speed: 10,
                rollingResistance: 5,
                airDensity: 1.225,
                frontalArea: 0.5,
                windSpeed: 0,
                drivetrainEfficiency: 95
            };

            // Initialize parameter values with defaults
            let tireSize = defaultParams.tireSize;
            let gearRatio = defaultParams.gearRatio;
            let slope = defaultParams.slope;
            let weight = defaultParams.weight;
            let speed = defaultParams.speed;
            let rollingResistance = defaultParams.rollingResistance;
            let airDensity = defaultParams.airDensity;
            let frontalArea = defaultParams.frontalArea;
            let windSpeed = defaultParams.windSpeed;
            let drivetrainEfficiency = defaultParams.drivetrainEfficiency;

            const G = 9.8; // Gravity acceleration (m/s^2)
            const INCH_TO_METER = 0.0254; // Conversion factor from inches to meters
            const DRAG_COEFFICIENT = 0.6; // Typical drag coefficient for a cyclist

            /**
             * Calculates individual power components at the wheel.
             * @param {number} currentSpeed - Current speed in km/h.
             * @param {number} currentSlope - Current slope in degrees.
             * @returns {object} An object containing gravity, rolling, and air power components.
             */
            function calculateIndividualPowers(currentSpeed, currentSlope) {
                // Convert speed from km/h to m/s
                const speed_ms = currentSpeed * 1000 / 3600;
                // Convert wind speed from km/h to m/s
                const wind_speed_ms = windSpeed * 1000 / 3600;
                // Convert slope from degrees to radians
                const slope_rad = currentSlope * Math.PI / 180;
                // Convert rolling resistance from percentage to decimal
                const rr_decimal = rollingResistance / 100;

                // Power to overcome gravity (W)
                const P_gravity = weight * G * speed_ms * Math.sin(slope_rad);

                // Power to overcome rolling resistance (W)
                const P_roll = weight * G * speed_ms * rr_decimal;

                // Power to overcome air resistance (W)
                // Relative speed is bicycle speed + headwind speed (or -tailwind speed)
                const relative_speed_ms = speed_ms + wind_speed_ms;
                const P_air = 0.5 * airDensity * frontalArea * DRAG_COEFFICIENT * Math.pow(relative_speed_ms, 3);

                return {
                    P_gravity: P_gravity,
                    P_roll: P_roll,
                    P_air: P_air
                };
            }

            /**
             * Calculates total power at the wheel.
             * @param {number} currentSpeed - Current speed in km/h.
             * @param {number} currentSlope - Current slope in degrees.
             * @returns {number} Total power at the wheel in Watts.
             */
            function calculatePowerAtWheel(currentSpeed, currentSlope) {
                const powers = calculateIndividualPowers(currentSpeed, currentSlope);
                return powers.P_gravity + powers.P_roll + powers.P_air;
            }

            /**
             * Calculates power at the crank, considering drivetrain efficiency.
             * @param {number} currentSpeed - Current speed in km/h.
             * @param {number} currentSlope - Current slope in degrees.
             * @returns {number} Power at the crank in Watts.
             */
            function calculatePowerAtCrank(currentSpeed, currentSlope) {
                const P_wheel = calculatePowerAtWheel(currentSpeed, currentSlope);
                // Convert drivetrain efficiency from percentage to decimal
                const efficiency_decimal = drivetrainEfficiency / 100;
                // If power at wheel is negative (downhill, braking), multiply by efficiency.
                // Otherwise, divide by efficiency for positive power (pedaling).
                return P_wheel < 0
                    ? P_wheel * efficiency_decimal
                    : P_wheel / efficiency_decimal;
            }

            /**
             * Calculates cadence (pedal revolutions per minute).
             * @param {number} currentSpeed - Current speed in km/h.
             * @returns {number} Cadence in RPM.
             */
            function calculateCadence(currentSpeed) {
                // Convert tire size from inches to meters (diameter)
                const wheel_diameter_m = tireSize * INCH_TO_METER;
                // Calculate wheel circumference in meters
                const wheel_circumference_m = Math.PI * wheel_diameter_m;

                // Convert speed from km/h to m/s
                const speed_ms = currentSpeed * 1000 / 3600;

                // Calculate wheel revolutions per second
                const wheel_rps = speed_ms / wheel_circumference_m;

                // Calculate crank revolutions per second based on gear ratio (front teeth / rear teeth)
                // If gearRatio = FrontTeeth / RearTeeth, then crank_rps = wheel_rps / gearRatio
                const crank_rps = wheel_rps / gearRatio;

                // Convert to Cadence (RPM)
                const cadence_rpm = crank_rps * 60;

                return cadence_rpm;
            }

            // Initialize Chart.js chart instances
            let slopePowerChart;
            let speedPowerChart;

            /**
             * Initializes the Chart.js instances for slope power and speed power charts.
             * @param {CanvasRenderingContext2D} slopeCtx - The 2D rendering context for the slope chart canvas.
             * @param {CanvasRenderingContext2D} speedCtx - The 2D rendering context for the speed chart canvas.
             */
            function initializeCharts(slopeCtx, speedCtx) {
                // Get current computed styles to match chart colors
                const bodyStyle = getComputedStyle(document.body);
                const textColor = bodyStyle.color;
                // Use CSS custom properties if defined, otherwise fallback to default colors
                const gridColor = bodyStyle.getPropertyValue('--chart-grid-color') || 'rgba(255, 255, 255, 0.1)';
                const borderColor = bodyStyle.getPropertyValue('--chart-border-color') || 'rgb(75, 192, 192)';
                const backgroundColor = bodyStyle.getPropertyValue('--chart-background-color') || 'rgba(75, 192, 192, 0.2)';

                // Slope Power Chart
                slopePowerChart = new Chart(slopeCtx, {
                    type: 'line',
                    data: {
                        labels: [],
                        datasets: [{
                            label: '所需动力 (W)',
                            data: [],
                            borderColor: borderColor,
                            backgroundColor: backgroundColor,
                            tension: 0.4,
                            fill: true,
                            pointRadius: 0,
                            pointHoverRadius: 5,
                        }]
                    },
                    options: {
                        responsive: true,
                        maintainAspectRatio: false,
                        interaction: {
                            mode: 'index',
                            intersect: false,
                        },
                        scales: {
                            x: {
                                title: {
                                    display: true,
                                    text: '坡度 (度)',
                                    color: textColor
                                },
                                grid: {
                                    color: gridColor
                                },
                                ticks: {
                                    color: textColor
                                },
                                min: 0, // Set X-axis min
                                max: 50 // Set X-axis max
                            },
                            y: {
                                title: {
                                    display: true,
                                    text: '所需动力 (W)',
                                    color: textColor
                                },
                                beginAtZero: true,
                                grid: {
                                    color: gridColor
                                },
                                ticks: {
                                    color: textColor
                                }
                            }
                        },
                        plugins: {
                            tooltip: {
                                callbacks: {
                                    title: function(context) {
                                        return `坡度: ${context[0].label}°`;
                                    },
                                    label: function(context) {
                                        let label = context.dataset.label || '';
                                        if (label) {
                                            label += ': ';
                                        }
                                        label += parseFloat(context.raw).toFixed(2) + ' W';
                                        return label;
                                    }
                                }
                            },
                            legend: {
                                labels: {
                                    color: textColor
                                }
                            },
                            zoom: {
                                pan: {
                                    enabled: true,
                                    mode: 'x',
                                    modifierKey: 'alt',
                                },
                                zoom: {
                                    wheel: {
                                        enabled: true,
                                    },
                                    pinch: {
                                        enabled: true
                                    },
                                    mode: 'x',
                                }
                            }
                        }
                    }
                });

                // Speed Power Chart
                speedPowerChart = new Chart(speedCtx, {
                    type: 'line',
                    data: {
                        labels: [],
                        datasets: [{
                            label: '所需动力 (W)',
                            data: [],
                            borderColor: borderColor,
                            backgroundColor: backgroundColor,
                            tension: 0.4,
                            fill: true,
                            pointRadius: 0,
                            pointHoverRadius: 5,
                        }]
                    },
                    options: {
                        responsive: true,
                        maintainAspectRatio: false,
                        interaction: {
                            mode: 'index',
                            intersect: false,
                        },
                        scales: {
                            x: {
                                title: {
                                    display: true,
                                    text: '行进速度 (km/h)',
                                    color: textColor
                                },
                                grid: {
                                    color: gridColor
                                },
                                ticks: {
                                    color: textColor
                                },
                                min: 0, // Set X-axis min
                                max: 50 // Set X-axis max
                            },
                            y: {
                                title: {
                                    display: true,
                                    text: '所需动力 (W)',
                                    color: textColor
                                },
                                beginAtZero: true,
                                grid: {
                                    color: gridColor
                                },
                                ticks: {
                                    color: textColor
                                }
                            }
                        },
                        plugins: {
                            tooltip: {
                                callbacks: {
                                    title: function(context) {
                                        return `速度: ${context[0].label} km/h`;
                                    },
                                    label: function(context) {
                                        let label = context.dataset.label || '';
                                        if (label) {
                                            label += ': ';
                                        }
                                        label += parseFloat(context.raw).toFixed(2) + ' W';
                                        return label;
                                    }
                                }
                            },
                            legend: {
                                labels: {
                                    color: textColor
                                }
                            },
                            zoom: {
                                pan: {
                                    enabled: true,
                                    mode: 'x',
                                    modifierKey: 'alt',
                                },
                                zoom: {
                                    wheel: {
                                        enabled: true,
                                    },
                                    pinch: {
                                        enabled: true
                                    },
                                    mode: 'x',
                                }
                            }
                        }
                    }
                });
            }

            /**
             * Updates all calculated results and Chart.js charts based on current input values.
             */
            function updateAllCalculationsAndCharts() {
                // Update all parameters from their respective input fields
                tireSize = parseFloat(tireSizeInput.value);
                gearRatio = parseFloat(gearRatioInput.value);
                slope = parseFloat(slopeInput.value);
                weight = parseFloat(weightInput.value);
                speed = parseFloat(speedInput.value);
                rollingResistance = parseFloat(rollingResistanceInput.value);
                airDensity = parseFloat(airDensityInput.value);
                frontalArea = parseFloat(frontalAreaInput.value);
                windSpeed = parseFloat(windSpeedInput.value);
                drivetrainEfficiency = parseFloat(drivetrainEfficiencyInput.value);

                // Calculate individual power components
                const powers = calculateIndividualPowers(speed, slope);

                // Update calculated results
                powerOutput.textContent = calculatePowerAtCrank(speed, slope).toFixed(2);
                gravityPowerOutput.textContent = powers.P_gravity.toFixed(2);
                rollingPowerOutput.textContent = powers.P_roll.toFixed(2);
                airPowerOutput.textContent = powers.P_air.toFixed(2);
                cadenceOutput.textContent = calculateCadence(speed).toFixed(2);

                // Update Slope Power Chart
                const slopeLabels = [];
                const slopeData = [];
                for (let s = 0; s <= 50; s += 0.5) { // Slope from 0 to 50 degrees, step 0.5
                    slopeLabels.push(s.toFixed(1));
                    slopeData.push(calculatePowerAtCrank(speed, s)); // Use current speed, vary slope
                }
                slopePowerChart.data.labels = slopeLabels;
                slopePowerChart.data.datasets[0].data = slopeData;
                slopePowerChart.update();

                // Update Speed Power Chart
                const speedLabels = [];
                const speedData = [];
                for (let v = 0; v <= 60; v += 1) { // Speed from 0 to 60 km/h, step 1
                    speedLabels.push(v);
                    speedData.push(calculatePowerAtCrank(v, slope)); // Use current slope, vary speed
                }
                speedPowerChart.data.labels = speedLabels;
                speedPowerChart.data.datasets[0].data = speedData;
                speedPowerChart.update();
            }

            // Array of input-slider pairs for easier iteration
            const inputSliderPairs = [
                { numInput: tireSizeInput, slider: tireSizeSlider },
                { numInput: gearRatioInput, slider: gearRatioSlider },
                { numInput: slopeInput, slider: slopeSlider },
                { numInput: weightInput, slider: weightSlider },
                { numInput: speedInput, slider: speedSlider },
                { numInput: rollingResistanceInput, slider: rollingResistanceSlider },
                { numInput: airDensityInput, slider: airDensitySlider },
                { numInput: frontalAreaInput, slider: frontalAreaSlider },
                { numInput: windSpeedInput, slider: windSpeedSlider },
                { numInput: drivetrainEfficiencyInput, slider: drivetrainEfficiencySlider }
            ];

            // Attach event listeners for number inputs and sliders
            inputSliderPairs.forEach(pair => {
                const numInput = pair.numInput;
                const slider = pair.slider;

                // Sync number input to slider and trigger update
                numInput.addEventListener('input', () => {
                    slider.value = numInput.value;
                    updateAllCalculationsAndCharts();
                });

                // Sync slider to number input and trigger update
                slider.addEventListener('input', () => {
                    numInput.value = slider.value;
                    updateAllCalculationsAndCharts();
                });

                // Mouse wheel event listener for number input
                numInput.addEventListener('wheel', (e) => {
                    e.preventDefault(); // Prevent page scroll
                    let currentValue = parseFloat(numInput.value);
                    let step = parseFloat(numInput.step) || 1; // Get step, default to 1
                    let min = parseFloat(numInput.min) || -Infinity;
                    let max = parseFloat(numInput.max) || Infinity;

                    if (e.deltaY < 0) { // Scroll up (increase value)
                        currentValue += step;
                    } else { // Scroll down (decrease value)
                        currentValue -= step;
                    }

                    // Limit within min/max range
                    currentValue = Math.max(min, Math.min(max, currentValue));

                    // Set input value and maintain decimal places
                    numInput.value = currentValue.toFixed(numInput.step.split('.')[1] ? numInput.step.split('.')[1].length : 0);
                    slider.value = numInput.value; // Sync slider
                    updateAllCalculationsAndCharts();
                });
            });


            // Reset button event listener
            resetParamsBtn.addEventListener('click', () => {
                // Reset number input values
                tireSizeInput.value = defaultParams.tireSize;
                gearRatioInput.value = defaultParams.gearRatio;
                slopeInput.value = defaultParams.slope;
                weightInput.value = defaultParams.weight;
                speedInput.value = defaultParams.speed;
                rollingResistanceInput.value = defaultParams.rollingResistance;
                airDensityInput.value = defaultParams.airDensity;
                frontalAreaInput.value = defaultParams.frontalArea;
                windSpeedInput.value = defaultParams.windSpeed;
                drivetrainEfficiencyInput.value = defaultParams.drivetrainEfficiency;

                // Also reset slider values
                tireSizeSlider.value = defaultParams.tireSize;
                gearRatioSlider.value = defaultParams.gearRatio;
                slopeSlider.value = defaultParams.slope;
                weightSlider.value = defaultParams.weight;
                speedSlider.value = defaultParams.speed;
                rollingResistanceSlider.value = defaultParams.rollingResistance;
                airDensitySlider.value = defaultParams.airDensity;
                frontalAreaSlider.value = defaultParams.frontalArea;
                windSpeedSlider.value = defaultParams.windSpeed;
                drivetrainEfficiencySlider.value = defaultParams.drivetrainEfficiency;

                // Trigger update after resetting values
                updateAllCalculationsAndCharts();
            });


            // Initial chart render and calculations
            initializeCharts(slopeChartCtx, speedChartCtx);
            updateAllCalculationsAndCharts(); // Perform initial calculation and chart update


            /**
             * Sets CSS variables for Chart.js theme based on system color scheme.
             * Destroys and re-initializes charts to apply new theme colors.
             */
            function setChartThemeVariables() {
                const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
                const root = document.documentElement;

                if (isDarkMode) {
                    root.style.setProperty('--chart-grid-color', 'rgba(255, 255, 255, 0.1)');
                    root.style.setProperty('--chart-border-color', 'rgb(76, 175, 80)'); // Green
                    root.style.setProperty('--chart-background-color', 'rgba(76, 175, 80, 0.2)');
                } else {
                    root.style.setProperty('--chart-grid-color', 'rgba(0, 0, 0, 0.1)');
                    root.style.setProperty('--chart-border-color', 'rgb(40, 167, 69)'); // Green
                    root.style.setProperty('--chart-background-color', 'rgba(40, 167, 69, 0.2)');
                }
                // Re-initialize charts to apply new colors
                if (slopePowerChart) {
                    slopePowerChart.destroy();
                }
                if (speedPowerChart) {
                    speedPowerChart.destroy();
                }
                initializeCharts(slopeChartCtx, speedChartCtx);
                updateAllCalculationsAndCharts();
            }

            // Listen for system theme changes
            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', setChartThemeVariables);
            // Set theme variables on page load
            setChartThemeVariables();


            /**
             * Creates a modal/popup containing a Chart.js chart.
             * @param {string} title - The title of the chart modal.
             * @param {string} xAxisLabel - Label for the X-axis.
             * @param {string} yAxisLabel - Label for the Y-axis.
             * @param {Array<number|string>} labels - Data labels for the chart.
             * @param {Array<number>} data - Data values for the chart.
             * @param {string} chartType - Type of the chart (e.g., 'line', 'bar').
             */
            function createChartModal(title, xAxisLabel, yAxisLabel, labels, data, chartType = 'line') {
                // Create modal background
                const modalBackground = document.createElement('div');
                modalBackground.style.cssText = `
                    position: fixed;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    background-color: rgba(0, 0, 0, 0.7);
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    z-index: 1000;
                `;
                modalBackground.onclick = (e) => {
                    if (e.target === modalBackground) {
                        modalBackground.remove();
                    }
                };

                // Create modal content
                const modalContent = document.createElement('div');
                modalContent.style.cssText = `
                    background-color: ${getComputedStyle(document.body).backgroundColor};
                    padding: 20px;
                    border-radius: 10px;
                    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
                    width: 80%;
                    max-width: 700px;
                    max-height: 80%;
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    position: relative;
                `;

                // Create close button
                const closeButton = document.createElement('button');
                closeButton.textContent = 'X';
                closeButton.style.cssText = `
                    position: absolute;
                    top: 10px;
                    right: 10px;
                    background: none;
                    border: none;
                    font-size: 1.2em;
                    color: ${getComputedStyle(document.body).color};
                    cursor: pointer;
                `;
                closeButton.onclick = () => modalBackground.remove();

                // Create title
                const modalTitle = document.createElement('h3');
                modalTitle.textContent = title;
                modalTitle.style.cssText = `
                    color: ${getComputedStyle(document.querySelector('h1')).color};
                    margin-top: 0;
                    margin-bottom: 20px;
                    text-align: center;
                `;

                // Create canvas for the chart
                const canvas = document.createElement('canvas');
                canvas.style.cssText = `
                    width: 100%;
                    height: 400px; /* Fixed height for modal chart */
                `;
                const ctx = canvas.getContext('2d');

                modalContent.appendChild(closeButton);
                modalContent.appendChild(modalTitle);
                modalContent.appendChild(canvas);
                modalBackground.appendChild(modalContent);
                document.body.appendChild(modalBackground);

                // Get current computed styles for chart colors
                const bodyStyle = getComputedStyle(document.body);
                const textColor = bodyStyle.color;
                const gridColor = bodyStyle.getPropertyValue('--chart-grid-color') || 'rgba(255, 255, 255, 0.1)';
                const borderColor = bodyStyle.getPropertyValue('--chart-border-color') || 'rgb(75, 192, 192)';
                const backgroundColor = bodyStyle.getPropertyValue('--chart-background-color') || 'rgba(75, 192, 192, 0.2)';


                new Chart(ctx, {
                    type: chartType,
                    data: {
                        labels: labels,
                        datasets: [{
                            label: yAxisLabel,
                            data: data,
                            borderColor: borderColor,
                            backgroundColor: backgroundColor,
                            tension: 0.4,
                            fill: true,
                            pointRadius: 0,
                            pointHoverRadius: 5,
                        }]
                    },
                    options: {
                        responsive: true,
                        maintainAspectRatio: false,
                        interaction: {
                            mode: 'index',
                            intersect: false,
                        },
                        scales: {
                            x: {
                                title: {
                                    display: true,
                                    text: xAxisLabel,
                                    color: textColor
                                },
                                grid: {
                                    color: gridColor
                                },
                                ticks: {
                                    color: textColor
                                },
                                min: labels[0],
                                max: labels[labels.length - 1]
                            },
                            y: {
                                title: {
                                    display: true,
                                    text: yAxisLabel,
                                    color: textColor
                                },
                                beginAtZero: true,
                                grid: {
                                    color: gridColor
                                },
                                ticks: {
                                    color: textColor
                                }
                            }
                        },
                        plugins: {
                            tooltip: {
                                callbacks: {
                                    title: function(context) {
                                        return `${xAxisLabel}: ${context[0].label}`;
                                    },
                                    label: function(context) {
                                        let label = context.dataset.label || '';
                                        if (label) {
                                            label += ': ';
                                        }
                                        label += parseFloat(context.raw).toFixed(2);
                                        return label;
                                    }
                                }
                            },
                            legend: {
                                labels: {
                                    color: textColor
                                }
                            },
                            zoom: {
                                pan: {
                                    enabled: true,
                                    mode: 'x',
                                    modifierKey: 'alt',
                                },
                                zoom: {
                                    wheel: {
                                        enabled: true,
                                    },
                                    pinch: {
                                        enabled: true
                                    },
                                    mode: 'x',
                                }
                            }
                        }
                    }
                });
            }

            // Get labels for the input elements for click events
            const labelsForCharts = [
                { id: 'slope', labelText: '坡度 (度)', calculator: (s) => calculatePowerAtCrank(speed, s), xAxis: '坡度 (度)', yAxis: '所需动力 (W)', min: 0, max: 50, step: 0.5, currentParam: 'slope' },
                { id: 'weight', labelText: '人车总重量 (kg)', calculator: (w) => { const originalWeight = weight; weight = w; const result = calculatePowerAtCrank(speed, slope); weight = originalWeight; return result; }, xAxis: '人车总重量 (kg)', yAxis: '所需动力 (W)', min: 10, max: 500, step: 5, currentParam: 'weight' },
                { id: 'speed', labelText: '行进速度 (km/h)', calculator: (v) => calculatePowerAtCrank(v, slope), xAxis: '行进速度 (km/h)', yAxis: '所需动力 (W)', min: 0, max: 60, step: 1, currentParam: 'speed' },
                { id: 'rolling-resistance', labelText: '滚动阻力 (%)', calculator: (rr) => { const originalRR = rollingResistance; rollingResistance = rr; const result = calculatePowerAtCrank(speed, slope); rollingResistance = originalRR; return result; }, xAxis: '滚动阻力 (%)', yAxis: '所需动力 (W)', min: 0, max: 20, step: 0.5, currentParam: 'rollingResistance' },
                { id: 'air-density', labelText: '空气密度 (kg/m³)', calculator: (ad) => { const originalAD = airDensity; airDensity = ad; const result = calculatePowerAtCrank(speed, slope); airDensity = originalAD; return result; }, xAxis: '空气密度 (kg/m³)', yAxis: '所需动力 (W)', min: 0.5, max: 2.0, step: 0.05, currentParam: 'airDensity' },
                { id: 'frontal-area', labelText: '迎风面积 (m²)', calculator: (fa) => { const originalFA = frontalArea; frontalArea = fa; const result = calculatePowerAtCrank(speed, slope); frontalArea = originalFA; return result; }, xAxis: '迎风面积 (m²)', yAxis: '所需动力 (W)', min: 0.1, max: 2.0, step: 0.05, currentParam: 'frontalArea' },
                { id: 'wind-speed', labelText: '风速 (km/h, 顺风为负)', calculator: (ws) => { const originalWS = windSpeed; windSpeed = ws; const result = calculatePowerAtCrank(speed, slope); windSpeed = originalWS; return result; }, xAxis: '风速 (km/h, 顺风为负)', yAxis: '所需动力 (W)', min: -30, max: 30, step: 1, currentParam: 'windSpeed' },
                { id: 'drivetrain-efficiency', labelText: '传动效率 (%)', calculator: (de) => { const originalDE = drivetrainEfficiency; drivetrainEfficiency = de; const result = calculatePowerAtCrank(speed, slope); drivetrainEfficiency = originalDE; return result; }, xAxis: '传动效率 (%)', yAxis: '所需动力 (W)', min: 50, max: 100, step: 1, currentParam: 'drivetrainEfficiency' },
                { id: 'tire-size', labelText: '轮胎尺寸 (英寸)', calculator: (ts) => { const originalTS = tireSize; tireSize = ts; const result = calculateCadence(speed); tireSize = originalTS; return result; }, xAxis: '轮胎尺寸 (英寸)', yAxis: '踏频 (RPM)', min: 20, max: 30, step: 0.5, currentParam: 'tireSize' },
                { id: 'gear-ratio', labelText: '牙盘传动比 (前/后)', calculator: (gr) => { const originalGR = gearRatio; gearRatio = gr; const result = calculateCadence(speed); gearRatio = originalGR; return result; }, xAxis: '牙盘传动比 (前/后)', yAxis: '踏频 (RPM)', min: 0.5, max: 3, step: 0.1, currentParam: 'gearRatio' }
            ];

            // Add click event listeners to labels dynamically
            labelsForCharts.forEach(item => {
                const labelElement = document.querySelector(`label[for="${item.id}"]`);
                if (labelElement) {
                    labelElement.addEventListener('click', () => {
                        const labels = [];
                        const data = [];
                        for (let val = item.min; val <= item.max; val += item.step) {
                            labels.push(val.toFixed(item.step.toString().split('.')[1] ? item.step.toString().split('.')[1].length : 0));
                            data.push(item.calculator(val));
                        }
                        createChartModal(
                            `${item.yAxis}随${item.xAxis.split('(')[0]}变化`, // Dynamic title
                            item.xAxis,
                            item.yAxis,
                            labels,
                            data
                        );
                    });
                }
            });
        });
    </script>
</body>
</html>